package xworker.protobuf;

import com.google.protobuf.AbstractMessage;
import com.google.protobuf.AbstractMessageLite;
import com.google.protobuf.util.JsonFormat;
import org.xmeta.ActionContext;
import org.xmeta.ActionException;
import org.xmeta.Thing;
import org.xmeta.cache.ThingObjectEntry;
import xworker.lang.executor.Executor;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ProtobufMessageFactory {
	private static final String TAG = ProtobufMessageFactory.class.getName();

	Thing thing;
	ActionContext actionContext;
	
	Map<String, Method> builders = new HashMap<>();
	Map<String, Method> liteBuilders = new HashMap<>();
	Map<String, BuilderFactory> builderFactoryMap = new HashMap<>();

	public ProtobufMessageFactory(){
	}

    /**
     * Creates a new instance.
     */
    public ProtobufMessageFactory(Thing thing, ActionContext actionContext) throws SecurityException {
    	this.thing = thing;
    	this.actionContext = actionContext;

    	Class<?>[] classes = thing.doAction("getClasses", actionContext);
    	if(classes != null) {
			regist(classes);
    	}
    }
    
    public  ProtobufMessageFactory(Class<?>[] classes, Thing thing, ActionContext actionContext) throws SecurityException {
    	this.thing = thing;
    	this.actionContext = actionContext;
    	
    	if(classes != null) {
    		regist(classes);
    	}
    }

    public void registBuilderFactory(String name, BuilderFactory builderFactory){
    	builderFactoryMap.put(name, builderFactory);
	}

    public List<String> getMessageNames(){
    	List<String> messages = new ArrayList<>();
    	for(String key : builders.keySet()){
    		if(!messages.contains(key)){
    			messages.add(key);
			}
		}

		for(String key : liteBuilders.keySet()){
			if(!messages.contains(key)){
				messages.add(key);
			}
		}

		return messages;
	}

    public Object decode(String messageType, String json) throws Exception {
    	Object objBuilder = newBuilder(messageType);
		JsonFormat.Parser format = JsonFormat.parser();
    	if(objBuilder instanceof com.google.protobuf.AbstractMessage.Builder<?>) {
    		com.google.protobuf.AbstractMessage.Builder<?> builder = (com.google.protobuf.AbstractMessage.Builder<?>) objBuilder;
    		format.merge(json, builder);
    		return builder.build();
    	}else if(objBuilder instanceof com.google.protobuf.AbstractMessageLite.Builder<?,?>) {
    		throw new ActionException("JsonFormat not support MessageLite, builder=" + objBuilder);
    		//com.google.protobuf.AbstractMessageLite.Builder<?,?> builder = (com.google.protobuf.AbstractMessageLite.Builder<?,?>) objBuilder;
    		//format.merge(json, builder);
    		//format.merge(bin, builder);
    		//return builder.build();
    	}else {
    		throw new ActionException("Unkown builder type, builder=" + objBuilder);
    	}
    }
    
    public Object decode(String messageType, byte[] array, int offset, int length) throws Exception {
    	Object objBuilder = newBuilder(messageType);
    	
    	if(objBuilder instanceof com.google.protobuf.AbstractMessage.Builder<?>) {
    		com.google.protobuf.AbstractMessage.Builder<?> builder = (com.google.protobuf.AbstractMessage.Builder<?>) objBuilder;
    		builder.mergeFrom(array, offset, length);
    		return builder.build();
    	}else if(objBuilder instanceof com.google.protobuf.AbstractMessageLite.Builder<?,?>) {
    		com.google.protobuf.AbstractMessageLite.Builder<?,?> builder = (com.google.protobuf.AbstractMessageLite.Builder<?,?>) objBuilder;
    		builder.mergeFrom(array, offset, length);
    		return builder.build();
    	}else {
    		throw new ActionException("Unkown builder type, builder=" + objBuilder);
    	}
    }
    
    public Object newBuilder(String messageType) throws Exception{
    	BuilderFactory builderFactory = builderFactoryMap.get(messageType);
    	if(builderFactory != null){
    		return builderFactory.newBuilder();
		}

    	Method build = builders.get(messageType);
    	if(build == null) {
    		build = liteBuilders.get(messageType);
    	}
    	if(build == null) {
    		Class<?> cls = thing.getMetadata().getCategory().getClassLoader().loadClass(messageType);
    		if(isMessage(cls)) {
				build = cls.getMethod("newBuilder");
				builders.put(cls.getName(), build);
				builders.put(cls.getSimpleName(), build);
			}else if(isMessageLite(cls)) {
				build = cls.getMethod("newBuilder");
				liteBuilders.put(cls.getName(), build);
				liteBuilders.put(cls.getSimpleName(), build);
			}
    	}
    	
    	if(build != null) {
    		return build.invoke(null);
    	}else {
    		return null;
    	}
    }
    
	public static boolean isMessage(Class<?> cls) {		
		Class<AbstractMessage> messageClass = AbstractMessage.class;
		if(cls == null) {
			return false;
		}
		
		if(cls.getName().equals(messageClass.getName())) {
			return true;
		}
		
		return isMessage(cls.getSuperclass());
	}
	
	@SuppressWarnings("rawtypes")
	public static boolean isMessageLite(Class<?> cls) {		
		Class<AbstractMessageLite> messageClass = AbstractMessageLite.class;
		if(cls == null) {
			return false;
		}
		
		if(cls.getName().equals(messageClass.getName())) {
			return true;
		}
		
		return isMessageLite(cls.getSuperclass());
	}


	public void regist(Class<?> cls){
		for(Class<?> messageCls : cls.getDeclaredClasses()) {
			try {
				if (isMessage(messageCls)) {
					Method build = messageCls.getMethod("newBuilder");
					builders.put(messageCls.getName(), build);
					builders.put(messageCls.getSimpleName(), build);
				} else if (isMessageLite(messageCls)) {
					Method build = messageCls.getMethod("newBuilder");
					liteBuilders.put(messageCls.getName(), build);
					liteBuilders.put(messageCls.getSimpleName(), build);
				}
			} catch (Exception ignored) {

			}
		}
	}

	public void regist(Class<?>[] classes) {
    	for(Class<?> cls : classes) {
    		regist(cls);
    	}
    }

    public void regist(ProtobufMessageFactory factory){
    	this.builders.putAll(factory.builders);
    	this.liteBuilders.putAll(factory.liteBuilders);
	}

    //xworker.com.google.protobuf.ProtobufMessageFactory/@actions/@getProtobufMessageFactory
    public static ProtobufMessageFactory getProtobufMessageFactory(ActionContext actionContext){
		Thing self = actionContext.getObject("self");
		String key = "__factory_cache__";
		ThingObjectEntry<ProtobufMessageFactory> entry = self.getData(key);
		if(entry == null){
			entry = new ThingObjectEntry<ProtobufMessageFactory>(self) {
				@Override
				protected ProtobufMessageFactory createObject(Thing thing) {
					//通过类初始化消息
					ProtobufMessageFactory factory = new ProtobufMessageFactory();
					factory.thing = thing;
					factory.actionContext = actionContext;

					//注册类
					Thing classLoaderThing = thing.doAction("getClassLoader", actionContext);
					ClassLoader classLoader = null;
					if(classLoaderThing != null){
						classLoader = classLoaderThing.doAction("getCategoryClassLoader", actionContext);
					}
					List<String> classes = thing.doAction("getClasses", actionContext);
					for(String clsName : classes){
						Class<?> cls = null;
						try{
							if(classLoader != null){
								cls = classLoader.loadClass(clsName);
							}
						}catch(Exception ignored){
						}

						try {
							if (cls == null) {
								cls = thing.getMetadata().getCategory().getClassLoader().loadClass(clsName);
							}
						}catch(Exception ignored){
						}

						if(cls == null){
							Executor.warn(TAG, "Can not regist class, className=" + clsName);
						}else{
							factory.regist(cls);
						}
					}

					//加入Proto模型定义的消息
					List<Thing> protos = thing.doAction("getProtos", actionContext);
					for(Thing proto : protos){
						ProtobufMessageFactory f = proto.doAction("getMessageFactory", actionContext);
						if(f != null){
							factory.regist(f);
						}
					}

					//通过子节点初始化
					for(Thing child : thing.getChilds()){
						child.doAction("init", actionContext, "messageFactoy", factory);
					}
					return factory;
				}
			};
			self.setData(key, entry);
		}

		return entry.getObject();
	}

	public String generateBuilderFactoryCode(){
    	StringBuilder stringBuilder = new StringBuilder();

    	for(String name : builders.keySet()){
    		Method method = builders.get(name);
			stringBuilder.append("        messageFactory.registBuilderFactory(\"").append(name).append("\", ")
					.append(method.getDeclaringClass().getName().replace('$', '.')).append("::newBuilder);\n");
    	}

		for(String name : liteBuilders.keySet()){
			Method method = liteBuilders.get(name);
			stringBuilder.append("        messageFactory.registBuilderFactory(\"").append(name).append("\", ")
					.append(method.getDeclaringClass().getName().replace('$', '.')).append("::newBuilder);\n");
		}

    	return stringBuilder.toString();
	}
}
